/*
 * The MIT License
 *
 * Copyright 2012 http://generic-toolset.googlecode.com.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package org.rlpw.swing;

import java.awt.Insets;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.dnd.Autoscroll;
import java.awt.dnd.DropTarget;
import java.awt.dnd.DropTargetAdapter;
import java.awt.dnd.DropTargetDragEvent;
import java.awt.dnd.DropTargetDropEvent;
import java.lang.reflect.InvocationTargetException;
import java.lang.annotation.Annotation;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.EventObject;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;
import javax.swing.DropMode;
import javax.swing.JTree;
import javax.swing.event.EventListenerList;
import javax.swing.event.TreeModelEvent;
import javax.swing.event.TreeModelListener;
import javax.swing.tree.TreeModel;
import javax.swing.tree.TreePath;
import javax.swing.tree.TreeSelectionModel;

import org.rlpw.swing.event.ModelEvent;
import org.rlpw.swing.event.ModelListener;
import org.rlpw.swing.event.ModelReplacedEvent;
import org.rlpw.swing.tree.DragAndDropFilter;
import org.rlpw.swing.tree.Children;
import org.rlpw.swing.tree.Parent;

/** 
 * 
 * @param <T> the type of the elements
 */
public class JOTree<T> extends JTree implements View<T>, Selectable<T>, Autoscroll {
    
    private Object root;
    private Model<T> model;
    private ReflectionTreeModel reflectionTreeModel;
    private DragAndDropFilter<T> dadf;
    private FilteredList<T> flist;
    private int autoscrollMargin = 15;

    /**
     * Constructs a empty <code>JOTree</code> which displays the default root node.
     * @param c the type of displayed items which is used for the reflection
     */
    public JOTree(Class<T> c) {
        this(c, new DefaultModel<T>());
    }

    /**
     * Constructs a <code>JOTree</code> which displays the default root node.
     * The tree is created using the specified data model.
     * @param c the type of displayed items which is used for the reflection
     * @param model the data model of displayed items
     */
    public JOTree(Class<T> c, Model<T> model) {
        this(c, model, "Root");
    }

    /**
     * Constructs a <code>JOTree</code> which displays the specified root node.
     * The tree is created using the specified data model.
     * @param c the type of displayed items which is used for the reflection
     * @param model the data model of displayed items
     * @param root displays the specified root node
     */
    public JOTree(Class<T> c, Model<T> model, Object root) {
        this.root = root;
        flist = new FilteredList<>(this);
        setDataModel(model);
        reflectionTreeModel = new TreeModelProvider(c).getTreeModel();
        setModel(reflectionTreeModel);
    }

    /**
     * Sets an {@code DragAndDropFilter} which used by the drag and drop support.
     * 
     * <p><b>Example:</b></p>
     * No items can be dragged to the root.
     * <pre>
     * <code>
     *  tree.setDragAndDropFilter(new DragAndDropFilter<anytype>() {
     *      public boolean acceptDrop(anytype child, Object parent) {
     *          return !parent.equals(tree.getRoot());
     *      }
     *  });
     * </code>
     * </pre>
     * @see DragAndDropFilter
     * @param dadf the DragAndDropFilter
     */
    public void setDragAndDropFilter(DragAndDropFilter<T> dadf) {
        this.dadf = dadf;
    }

    /**
     * Turns on or off the drag and drop support.
     * <p/>
     * The drag and drop support is based on reflection. 
     * The shifts are stored directly in the objects. 
     * @param enabled whether or not to enable drag and drop support
     */
    public void setDragAndDropSupport(boolean enabled) {
        if (enabled && !reflectionTreeModel.isSettable()) {
            throw new RuntimeException("The annotation must be set to a field to use drag and drop.");
        }
        setDragEnabled(enabled);
        if (enabled) {
            //<editor-fold defaultstate="collapsed" desc="Drag and Drop Support">
            setDropMode(DropMode.USE_SELECTION);
            
            setDropTarget(new DropTarget(this, new DropTargetAdapter() {
                
                TreePath pathLast;
                Timer expandedTimer;
                List<T> selected;
                SelectModel<T> selectModel = getSelectModel();
                boolean isRootSelected = false;
                
                @Override
                public void drop(DropTargetDropEvent dtde) {
                    try {
                        if (expandedTimer != null) {
                            expandedTimer.cancel();
                        }
                        selectModel.setAllSelected(false);
                        for (T child : selected) {
                            reflectionTreeModel.removeFromParent(child);
                            reflectionTreeModel.insertIntoParent(child, getDropItem(dtde.getLocation()));
                            selectModel.setSelected(child, true);
                        }
                        getDataModel().updateViews();
                    } catch (Exception excep) {
                    }
                    dtde.dropComplete(true);
                }
                
                private Object getDropItem(Point p) {
                    return JOTree.this.getPathForRow(JOTree.this.getRowForLocation(p.x, p.y)).getLastPathComponent();
                }
                
                @Override
                public void dragEnter(DropTargetDragEvent dtde) {
                    isRootSelected = getSelectionModel().isPathSelected(new TreePath(getRoot()));
                    selected = selectModel.getSelected();
                }
                
                @Override
                public void dragOver(DropTargetDragEvent dtde) {
                    if (isRootSelected) {
                        dtde.rejectDrag();
                        return;
                    }

                    //auto expanded support:
                    TreePath path = getClosestPathForLocation(dtde.getLocation().x, dtde.getLocation().y);
                    if (!(path == pathLast)) {
                        pathLast = path;
                        
                        if (expandedTimer != null) {
                            expandedTimer.cancel();
                        }
                        expandedTimer = new Timer();
                        expandedTimer.schedule(new TimerTask() {
                            
                            @Override
                            public void run() {
                                if (pathLast.getLastPathComponent().equals(getRoot())) {
                                    return;
                                }
                                if (isExpanded(pathLast)) {
                                    collapsePath(pathLast);
                                } else {
                                    expandPath(pathLast);
                                }
                            }
                        }, 1000);
                    }

                    //check dropItem:
                    try {
                        Object dropItem = getDropItem(dtde.getLocation());
                        for (T child : selected) {
                            Object parent = dropItem;
                            try {
                                if (dadf != null && !dadf.acceptDrop(child, parent)) {
                                    dtde.rejectDrag();
                                    return;
                                }
                            } catch (UnsupportedOperationException e) {
                            }
                            if (dropItem.equals(getRoot())) {
                                continue;
                            }
                            while (!parent.equals(getRoot())) {
                                if (child.equals(parent)) {
                                    dtde.rejectDrag();
                                    return;
                                }
                                parent = reflectionTreeModel.getParent((T) parent);
                                if (parent == null) {
                                    parent = getRoot();
                                }
                            }
                        }
                        
                        dtde.acceptDrag(dtde.getDropAction());
                    } catch (Exception excep) {
                        dtde.rejectDrag();
                    }
                }
            }));
            //</editor-fold>
        } else {
            setDropTarget(null);
        }
        
    }

    /**
     * Sets the root of the tree.
     * 
     * @param root the root of the tree
     */
    public void setRoot(Object root) {
        if (model == null) {
            throw new IllegalArgumentException("Cannot set a null root");
        }
        this.root = root;
        updateView();
    }

    /**
     * Returns the root of the tree.
     *
     * @return the root of the tree
     */
    public Object getRoot() {
        return root;
    }

    /** {@inheritDoc} */
    @Override
    public void updateView() {
        if (flist != null) {
            flist.update();
        }
        if (reflectionTreeModel != null) {
            reflectionTreeModel.fireTreeStructureChanged();
        }
    }

    /** {@inheritDoc} */
    @Override
    public void setDataModel(Model<T> model) {
        if (model == null) {
            throw new IllegalArgumentException("Cannot set a null Model");
        }
        if (this.model != null) {
            this.model.removeView(this);
        }
        model.addView(this);
        this.model = model;
        updateView();
    }

    /** {@inheritDoc} */
    @Override
    public Model<T> getDataModel() {
        return model;
    }

    /** {@inheritDoc} */
    @Override
    public SelectModel<T> getSelectModel() {
        return new SelectModelImpl();
    }

    /** {@inheritDoc} */
    @Override
    public Filter<T> getFilter() {
        return flist.getFilter();
    }

    /** {@inheritDoc} */
    @Override
    public void setFilter(Filter<T> filter) {
        flist.setFilter(filter);
    }

    /** {@inheritDoc} */
    @Override
    public void setSorter(Comparator<T> comp) {
        flist.setSorter(comp);
    }

    /** {@inheritDoc} */
    @Override
    public Comparator<T> getSorter() {
        return flist.getSorter();
    }

    /** {@inheritDoc} */
    @Override
    public List<T> getVisibleItems() {
        return flist;
    }

    /** {@inheritDoc} */
    @Override
    public void autoscroll(Point p) {
        int realrow = getRowForLocation(p.x, p.y);
        Rectangle outer = getBounds();
        realrow = (p.y + outer.y <= autoscrollMargin ? realrow < 1 ? 0 : realrow - 1
                : realrow < getRowCount() - 1 ? realrow + 1 : realrow);
        scrollRowToVisible(realrow);
    }

    /** {@inheritDoc} */
    @Override
    public Insets getAutoscrollInsets() {
        Rectangle outer = getBounds();
        Rectangle inner = getParent().getBounds();
        return new Insets(inner.y - outer.y + autoscrollMargin, inner.x - outer.x
                + autoscrollMargin, outer.height - inner.height - inner.y + outer.y
                + autoscrollMargin, outer.width - inner.width - inner.x + outer.x
                + autoscrollMargin);
    }
    
    private void fireChanged(T item) {
        if (getDataModel() instanceof DefaultModel) {
            ((DefaultModel<T>) getDataModel()).fireChanged(item, getDataModel().indexOf(item));
        }
    }
    
    private class TreeModelProvider {
        
        private ReflectionTreeModel treeModel;
        private int count;
        private Class<T> clazz;
        
        public TreeModelProvider(Class<T> c) {
            clazz = c;
            for (Field f : c.getDeclaredFields()) {
                setModel(f);
            }
            for (Method m : c.getDeclaredMethods()) {
                setModel(m);
            }
            
            if (count != 1) {
                throw new RuntimeException("Invalid number of parent or children Annotations in '" + c.getName() + "'.");
            }
        }
        
        private void setModel(AccessibleObject ao) {
            if (ao.isAnnotationPresent(Parent.class)) {
                treeModel = new ParentLogic(clazz);
                count++;
            }
            if (ao.isAnnotationPresent(Children.class)) {
                treeModel = new ChildrenLogic(clazz);
                count++;
            }
        }
        
        public ReflectionTreeModel getTreeModel() {
            return treeModel;
        }
    }
    
    abstract class ReflectionTreeModel implements TreeModel {
        
        protected ReflectionData data;
        protected EventListenerList listenerList = new EventListenerList();
        private Map<Object, List<T>> children = new HashMap<>();
        
        public ReflectionTreeModel() {
            
            getDataModel().addModelListener(new ModelListener<T>() {
                
                @Override
                public void itemAdded(ModelEvent<T> e) {
                    children.clear();
                }
                
                @Override
                public void itemRemoved(ModelEvent<T> e) {
                    children.clear();
                }
                
                @Override
                public void itemChanged(ModelEvent<T> e) {
                    children.clear();
                }
                
                @Override
                public void itemReplaced(ModelReplacedEvent<T> e) {
                    children.clear();
                }
                
                @Override
                public void allItemsRemoved(EventObject e) {
                    children.clear();
                }
            });
            
            addTreeModelListener(new TreeModelListener() {
                
                @Override
                public void treeNodesChanged(TreeModelEvent e) {
                    children.remove(e.getTreePath().getLastPathComponent());
                }
                
                @Override
                public void treeNodesInserted(TreeModelEvent e) {
                    children.remove(e.getTreePath().getLastPathComponent());
                }
                
                @Override
                public void treeNodesRemoved(TreeModelEvent e) {
                    children.remove(e.getTreePath().getLastPathComponent());
                }
                
                @Override
                public void treeStructureChanged(TreeModelEvent e) {
                    children.remove(e.getTreePath().getLastPathComponent());
                }
            });
            
        }
        
        @Override
        public Object getRoot() {
            return JOTree.this.getRoot();
        }
        
        @Override
        public Object getChild(Object parent, int index) {
            return getChildren(parent).get(index);
        }
        
        @Override
        public int getChildCount(Object parent) {
            return getChildren(parent).size();
        }
        
        @Override
        public boolean isLeaf(Object node) {
            return getChildCount(node) == 0;
        }
        
        @Override
        public void valueForPathChanged(TreePath path, Object newValue) {
        }
        
        @Override
        public int getIndexOfChild(Object parent, Object child) {
            return getChildren(parent).indexOf(child);
        }
        
        @Override
        public void addTreeModelListener(TreeModelListener l) {
            listenerList.add(TreeModelListener.class, l);
        }
        
        @Override
        public void removeTreeModelListener(TreeModelListener l) {
            listenerList.remove(TreeModelListener.class, l);
        }
        
        protected void fireTreeStructureChanged() {
            children.clear();
            List<TreePath> expands = new LinkedList<>();
            for (int i = 0; i < getRowCount(); i++) {
                TreePath path = getPathForRow(i);
                if (isExpanded(path)) {
                    expands.add(path);
                }
            }
            Object[] listeners = listenerList.getListenerList();
            TreeModelEvent e = null;
            for (int i = listeners.length - 2; i >= 0; i -= 2) {
                if (listeners[i] == TreeModelListener.class) {
                    if (e == null) {
                        e = new TreeModelEvent(this, new TreePath(root));
                    }
                    ((TreeModelListener) listeners[i + 1]).treeStructureChanged(e);
                }
            }
            for (TreePath path : expands) {
                expandPath(path);
            }
        }
        
        TreePath getTreePath(T item) {
            if (item == null) {
                return null;
            }
            if (item.equals(root)) {
                return new TreePath(root);
            }
            List ls = new LinkedList();
            
            while (item != null) {
                ls.add(0, item);
                item = getParent(item);
            }
            ls.add(0, root);
            return new TreePath(ls.toArray());
        }
        
        List<T> getChildren(Object parent) {
            if (!children.containsKey(parent)) {
                if (parent.equals(getRoot())) {
                    children.put(parent, getRootChildren());
                } else {
                    children.put(parent, getNodeChildren(parent));
                }
            }
            return children.get(parent);
            
        }
        
        abstract List<T> getNodeChildren(Object parent);
        
        abstract List<T> getRootChildren();
        
        abstract T getParent(T child);
        
        abstract void removeFromParent(T child);
        
        abstract void insertIntoParent(T newChild, Object parent);
        
        boolean isSettable() {
            return data.isSettable();
        }
    }
    
    class ParentLogic extends ReflectionTreeModel {
        
        public ParentLogic(Class<T> c) {
            data = new ReflectionScanner(c, Parent.class).getReflectionData();
        }
        
        @Override
        List<T> getNodeChildren(Object parent) {
            List<T> ls = new ArrayList<>();
            for (T item : getVisibleItems()) {
                T p = getParent(item);
                if (p != null && p.equals(parent)) {
                    ls.add(item);
                }
            }
            return ls;
        }
        
        @Override
        List<T> getRootChildren() {
            List<T> ls = new ArrayList<>();
            for (T item : getVisibleItems()) {
                T parent = getParent(item);
                if (parent == null
                        || !getVisibleItems().contains(parent)) {
                    ls.add(item);
                }
            }
            return ls;
        }
        
        @Override
        T getParent(T child) {
            T parent = data.get(child);
            if (getVisibleItems().contains(parent)) {
                return parent;
            } else {
                return null;
            }
        }
        
        @Override
        void removeFromParent(T child) {
            data.set(child, null);
        }
        
        @Override
        void insertIntoParent(T newChild, Object parent) {
            if (parent.equals(getRoot())) {
                parent = null;
            }
            data.set(newChild, parent);
        }
    }
    
    class ChildrenLogic extends ReflectionTreeModel {
        
        public ChildrenLogic(Class<T> c) {
            data = new ReflectionScanner(c, Children.class).getReflectionData();
        }
        
        @Override
        List<T> getNodeChildren(Object parent) {
            List<T> ls = (List<T>) data.getCollection(parent);
            ls.retainAll(getVisibleItems());
            return ls;
        }
        
        @Override
        List<T> getRootChildren() {
            Set items = new HashSet();
            for (T item : getVisibleItems()) {
                items.addAll(data.getCollection(item));
            }
            
            List<T> ls = new ArrayList<>();
            for (T item : getVisibleItems()) {
                if (!items.contains(item)) {
                    ls.add(item);
                }
            }
            return ls;
        }
        
        @Override
        T getParent(T child) {
            for (T parent : getVisibleItems()) {
                if (data.getCollection(parent).contains(child)) {
                    return parent;
                }
            }
            return null;
        }
        
        @Override
        void removeFromParent(T child) {
            T parent = getParent(child);
            if (parent == null) {
                return; // remove from data model?
            }
            Collection<T> collection = data.getCollection(parent);
            collection.remove(child);
            if (data.get(parent) == null) {
                data.set(parent, collection);
            }
        }
        
        @Override
        void insertIntoParent(T newChild, Object parent) {
            if (parent == null || parent.equals(getRoot())) {
                return;
            }
            Collection<T> collection = data.getCollection(parent);
            collection.add(newChild);
            data.set(parent, collection);
        }
    }

    //<editor-fold defaultstate="collapsed" desc="Reflection">
    private class ReflectionScanner {
        
        private ReflectionData reflectionData;
        
        public ReflectionScanner(Class<T> c, Class<? extends Annotation> anno) {
            for (Field field : c.getDeclaredFields()) {
                if (field.isAnnotationPresent(anno)) {
                    field.setAccessible(true);
                    reflectionData = new FieldData(field);
                    break;
                }
            }
            for (Method method : c.getDeclaredMethods()) {
                if (method.isAnnotationPresent(anno)) {
                    if (method.getParameterTypes().length > 0
                            || method.getReturnType() == void.class) {
                        throw new RuntimeException("");
                    }
                    
                    method.setAccessible(true);
                    reflectionData = new MethodData(method);
                    break;
                }
            }
            
        }
        
        public ReflectionData getReflectionData() {
            return reflectionData;
        }
    }
    
    private abstract class ReflectionData {
        
        public abstract T get(Object obj);
        
        public Collection<T> getCollection(Object obj) {
            if (get(obj) == null) {
                return new LinkedList<>();
            }
            
            if (get(obj) instanceof Collection) {
                return (Collection) get(obj);
            }
            Set items = new HashSet();
            items.add(get(obj));
            return items;
        }
        
        public abstract boolean isSettable();
        
        public abstract void set(Object obj, Object value);
    }
    
    private class MethodData extends ReflectionData {
        
        private Method method;
        
        public MethodData(Method method) {
            this.method = method;
        }
        
        @Override
        public T get(Object obj) {
            try {
                return (T) method.invoke(obj);
            } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) {
                throw new RuntimeException(ex.getMessage());
            }
        }
        
        @Override
        public boolean isSettable() {
            return false;
        }
        
        @Override
        public void set(Object obj, Object value) {
        }
    }
    
    private class FieldData extends ReflectionData {
        
        private Field field;
        
        public FieldData(Field field) {
            this.field = field;
        }
        
        @Override
        public T get(Object obj) {
            try {
                return (T) field.get(obj);
            } catch (IllegalArgumentException | IllegalAccessException ex) {
                throw new RuntimeException(ex.getMessage());
            }
        }
        
        @Override
        public boolean isSettable() {
            return true;
        }
        
        @Override
        public void set(Object obj, Object value) {
            try {
                field.set(obj, value);
                fireChanged((T) obj);
            } catch (IllegalArgumentException | IllegalAccessException ex) {
                throw new RuntimeException(ex.getMessage());
            }
        }
    }
    //</editor-fold>

    private class SelectModelImpl implements SelectModel<T> {
        
        @Override
        public boolean isSelected(int index) {
            rangeCheck(index);
            return isSelected(getVisibleItems().get(index));
        }
        
        @Override
        public boolean isSelected(T element) {
            for (TreePath tp : getSelectionPaths()) {
                if (tp.getLastPathComponent().equals(element)) {
                    return true;
                }
            }
            return false;
        }
        
        @Override
        public void setSelected(int index, boolean select) {
            rangeCheck(index);
            setSelected(getVisibleItems().get(index), select);
        }
        
        @Override
        public void setSelected(T element, boolean select) {
            if (select) {
                addSelectionPath(reflectionTreeModel.getTreePath(element));
            } else {
                removeSelectionPath(reflectionTreeModel.getTreePath(element));
            }
        }
        
        @Override
        public void setAllSelected(boolean select) {
            if (!select) {
                clearSelection();
            } else {
                List<T> visibleItems = getVisibleItems();
                List<TreePath> paths = new LinkedList<>();
                for (T item : visibleItems) {
                    paths.add(reflectionTreeModel.getTreePath(item));
                }
                setSelectionPaths(paths.toArray(new TreePath[0]));
            }
        }
        
        @Override
        public List<T> getSelected() {
            List<T> selected = new ArrayList<>();
            for (TreePath tp : getSelectionPaths()) {
                if (tp.getLastPathComponent() != getRoot()) {
                    selected.add((T) tp.getLastPathComponent());
                }
            }
            return selected;
        }
        
        @Override
        public int countSelected() {
            return getSelected().size();
        }
        
        @Override
        public void setMode(int mode) {
            if (mode == SelectionMode.SINGLE) {
                mode = TreeSelectionModel.SINGLE_TREE_SELECTION;
            } else if (mode == SelectionMode.MULTIPLE) {
                mode = TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION;
            } else {
                mode = TreeSelectionModel.CONTIGUOUS_TREE_SELECTION;
            }
            getSelectionModel().setSelectionMode(mode);
        }
        
        @Override
        public int getMode() {
            int mode = getSelectionModel().getSelectionMode();
            if (mode == TreeSelectionModel.SINGLE_TREE_SELECTION) {
                return SelectionMode.SINGLE;
            } else if (mode == TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION) {
                return SelectionMode.MULTIPLE;
            }
            return SelectionMode.INTERVAL;
        }
        
        private void rangeCheck(int index) {
            if (index >= getVisibleItems().size()) {
                throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + getVisibleItems().size());
            }
        }
    }
}
